Skip to content

test(cliffs): the 31 are TWO defects, and the tile resolver is neither (#84) - #115

Merged
wormeyman merged 1 commit into
mainfrom
cliffs/residual-splits-in-two
Aug 3, 2026
Merged

test(cliffs): the 31 are TWO defects, and the tile resolver is neither (#84)#115
wormeyman merged 1 commit into
mainfrom
cliffs/residual-splits-in-two

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

Follow-up to #114, which reduced the whole Vulcanus cliff residual to 31 destruction disagreements and handed over one question: Surface::wouldCollide runs constCollideWithTile against the real surface while the port resolves tiles from our own Vulcanus model, so a disagreement between the two inside a cliff's box would produce exactly this two-sided error set.

It read as needing a new headless capture. It needed none.

The tile resolver is exonerated for all 6 false rejections

oracle-vulcanus-lava-boundary is a committed 994-position dense capture of surface.get_tile(x, y).name from a real 2.1.12 Vulcanus surface. It was taken for a different question - the 35 tiles our mask called lava inside a real cliff's box, back when the collision box was the defect - and it covers every tile of all six of today's false-rejection boxes.

70 of 70 covered. Zero disagreements, both directions. The game read the same lava out of those boxes that we do and placed the cliff anyway. The box or the rule is holding the six, not the resolver.

Vacuity arms are in the spec and they matter here: "0 mismatches" is also what a comparison that never ran would print. Every one of the six boxes does contain lava by our model, and the fixture carries both lava and non-lava names.

A second candidate died in factorio-data @ 2.1.12 rather than in a capture: tiles-vulcanus.lua gives 17 tiles tile_collision_masks.ground() and exactly 2 - lava and lava-hot - tile_collision_masks.lava(). So VULCANUS_CLIFF_BLOCKING_TILES is provably complete.

The 25 in the other direction do NOT share a cause

Chebyshev distance from each cell's collision box to the nearest tile our own model calls lava, scored against the 1525 cells the port gets right so there is a base rate:

distance to our lava missed (25) matched (1525)
within 2 tiles 9 (36%) 52 (3.4%)
4 to 11 tiles 6 436
none within 12 10 1037

The near group is enriched 10.5x - the signature of a lava boundary or a box a tile short, and the one place a small change to the box could be right.

But ten of the 25 have no lava within twelve tiles. No adjustment to a lava collision box can reach them; neither can the ore rule (all 25 are ore = false) nor any entity (#111's autoplace_settings lever moved zero cliffs). They also cluster where the near group does not - 1746,{1530.5, 1534.5, 1538.5} is a vertical run of three, 1542/1546,{1550.5..1558.5} a knot, 1622/1626,1614.5 a pair.

Note "far from lava" is the common case (1037 of 1525 correct cells), so the far group carries no signal by itself - the near group is the unusual one. What the split establishes is only that one mechanism cannot produce both, which is enough to stop anyone tuning a box against all 25 at once.

So #113's framing was too narrow. "Which cells does Surface::wouldCollide reject that ours does not" is the right question for at most 15 of the 25 and for none of the 6. Fitting one collision box to all 31 would be fitting a rule to two causes at once - the failure #88 exists to record.

Refuted on the way: cliff-versus-cliff collision

Recorded rather than left to be re-derived, because it is the first idea the far ten suggest and a good one on its face: applyCliffs adds each cliff to the surface immediately after testing it, so cliff N+1's wouldCollide sees cliffs 1..N already there - and #111's lever cannot remove cliffs, so this is the one case it never covered. The far ten cluster, which is what a neighbour-versus-neighbour rule produces.

It dies on the base rate. 9 of the far 10 overlap another cliff's rotbb box - and so do 1405 of the 1531 cliffs the game keeps, 91.8%. Not enriched; marginally below base rate. A rule destroying on box overlap would have destroyed nearly every cliff on the map.

Independently: the cliff prototype's generic collision_box is {{-0.99,-0.49},{0.99,0.49}} - entity-util.lua calls it "intentionally small" - and cliff cells sit on a 4-tile grid, so two cliffs' generic boxes cannot overlap at all.

Next

The far ten are the bigger half and they are an unidentified mechanism, not a mistuned parameter. Everything named so far is ruled out for them.

Nothing shipping changes; this is measurement only. pnpm run verify green (1423 tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KVXnqZPGsD7A2SaPVA9ArA

#84)

#114 reduced the residual to 31 destruction disagreements and handed over one
question: Surface::wouldCollide runs constCollideWithTile against the REAL
surface while the port resolves tiles from our own Vulcanus model, so a
disagreement between the two inside a cliff's box would produce exactly this
two-sided error set. It read as needing a new capture. It needed none.

The tile half is exonerated for all 6 false rejections.
oracle-vulcanus-lava-boundary is a committed 994-position dense capture of
surface.get_tile(x, y).name, taken for a different question back when the
collision box was the defect, and it covers every tile of all six boxes: 70 of
70 covered, ZERO disagreements, both directions. The game read the same lava we
do and placed the cliff anyway. Vacuity arms included, because "0 mismatches" is
also what a comparison that never ran would print.

A second candidate died in factorio-data rather than in a capture:
tiles-vulcanus.lua gives 17 tiles ground() and exactly 2 - lava and lava-hot -
lava(), so VULCANUS_CLIFF_BLOCKING_TILES is provably complete.

The 25 in the other direction do NOT share a cause. By Chebyshev distance from
each box to the nearest tile our own model calls lava, scored against the 1525
cells the port gets right so there is a base rate:

  within 2 tiles   missed 9 (36%)   matched 52 (3.4%)
  4 to 11 tiles    missed 6         matched 436
  none within 12   missed 10        matched 1037

The near group is enriched 10.5x - the signature of a boundary or a box a tile
short. But ten of the 25 have no lava within twelve tiles, so no adjustment to a
lava collision box can reach them; neither can the ore rule (all 25 are
ore=false) nor any entity (#111's lever moved zero cliffs). They also cluster
where the near group does not.

So the handover framing was too narrow: "which cells does wouldCollide reject
that ours does not" is the right question for at most 15 of the 25 and for none
of the 6. Fitting one box to all 31 would be fitting a rule to two causes at
once, which is the failure #88 exists to record.

Refuted on the way, and recorded so nobody re-derives it: cliff-versus-cliff
collision. Attractive because applyCliffs adds each cliff to the surface right
after testing it, and because #111's autoplace_settings lever cannot remove
cliffs - so this is the one case that lever never covered. It dies on the base
rate: 9 of the far 10 overlap another cliff's rotbb box, and so do 1405 of the
1531 cliffs the game KEEPS (91.8%). Independently, the cliff prototype's generic
collision_box is {{-0.99,-0.49},{0.99,0.49}} - "intentionally small" - and cliff
cells sit on a 4-tile grid, so generic boxes cannot overlap at all.

Nothing shipping changes; this is measurement only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVXnqZPGsD7A2SaPVA9ArA
@wormeyman
wormeyman merged commit 2c2e5da into main Aug 3, 2026
2 checks passed
@wormeyman
wormeyman deleted the cliffs/residual-splits-in-two branch August 3, 2026 21:42
wormeyman added a commit that referenced this pull request Aug 3, 2026
#115 handed over two threads on the far ten and named the first unmeasured:
`applyCliffs` assumes destruction, but a strict superset says nothing about
whether the game's crossing field emitted those cells at all.

It is decidable from a fixture already on disk, because the two hypotheses
leave different marks on the NEIGHBOURS. `Cliff::onDestroy` trims a connected
neighbour's facing end unconditionally; a cell that was never queued only costs
its neighbour that end if `updateConnections` runs on it, and that is gated on
the neighbour sitting on its chunk's outer ring. So a disputed cell with a
non-border neighbour the game KEPT is settled by the game's own orientation.

- `1546,1550.5` and `1746,1538.5` are decidable and both say DESTROYED, one in
  each of the far group's two multi-cell clusters. Across all 225 cells the
  game destroys, the never-queued signature appears ZERO times.
- The counterfactual is what makes that more than reading a table: removing
  either from the QUEUE contradicts the game at its neighbour (wrong 1 each,
  2 together), while removing four far cells with no decidable neighbour
  changes nothing.
- #114's `1531 of 1531` is informative for only 14 of the 225 destructions and
  VACUOUS for the other 211. Those 14 are exactly #114's 14 no-cascade
  rewrites, cell for cell - two independent routes to the same set.
- The verdict is CONDITIONAL on the chunk-border gate, which
  `cliffConnections.spec.ts` records as unscored and which has been inert until
  now. Switch it off and both counterfactuals stop disagreeing with the game.
  Planting `onChunkBorder` returning true fails 7 of this file's 10 tests. If
  the gate was misread those cells revert to UNDECIDABLE, not to never-queued.

So the search stays on the unmodelled half of `Surface::wouldCollide` and not
on the crossing field, which #107 already exonerated once.

Measurement only - nothing shipping changed; recall 0.9961, precision 0.9858.


Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 4, 2026
#115 ruled the ore out for the whole residual in one clause: "no adjustment to
a lava collision box can ever reach them, and neither can the ore rule (all 25
are `ore = false`)".

`ore = false` there is OUR predicate's output, not the game's behaviour, and
`vulcanusOreRejection.ts` says on its own front page that the rule is "exactly
right where it fires, simply too narrow" - it accounts for 20 of the 31 cells
the ore actually suppresses. Ruling the ore out with it is circular. The
non-circular instrument was already on disk and already covers the right
region: `oracle-vulcanus-cliff-ore-direction` re-runs `[1500,1500]`, where
every one of the far ten lives, with the resources switched off.

Switch them off and SIX OF THE FAR TEN APPEAR.

- Of the 25: 11 ore (7 calcite + 4 geyser), 11 unknown, 3 outside the lever's
  region. The unidentified-mechanism population is 11, not 25.
- The far ten's clusters have DIFFERENT causes - the `1542/1546` knot is the
  geyser, the two singletons are calcite, and only the `1742/1746` vertical run
  of four survives as unexplained.
- #115's lava-distance grouping is not the causal grouping: ore cells land in
  its near, mid and far groups alike. Do not act on the near/far split.
- The 0.710 and 0.645 recall figures are the PLACED and RAW stages of the same
  predicate, not a disagreement. Precision is 1.000 at both.
- Sharpens #122: of its two proven destructions, `1546,1550.5` is a geyser
  suppression, so the unknown group's destruction proof rests on `1746,1538.5`.

The ore gap itself is not new - `vulcanusOreRejection.ts` has documented the
narrowness since it was written, with the geyser arm excluded deliberately.
What is new is that this known gap is where 11 of the 25 went.

#115's careful half stands and is untouched: the distances are right, no lava
box reaches the far group, and the tile resolver is exonerated 70/70. Its
doc comment is corrected in place rather than left to mislead.

Measurement only - nothing shipping changed.


Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant